import pandas as pd
import numpy as np
import seaborn as sns
from sklearn.model_selection import train_test_split
import xgboost as xgb
import shap
import dalex as dx
np.random.seed = 42
df_wines = pd.read_csv('./src/winequality-red.csv')
df_wines["is_good"]=df_wines.apply(lambda row: 1 if row.quality > 5 else 0, axis = 1)
df_wines.head()
| fixed acidity | volatile acidity | citric acid | residual sugar | chlorides | free sulfur dioxide | total sulfur dioxide | density | pH | sulphates | alcohol | quality | is_good | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 7.4 | 0.70 | 0.00 | 1.9 | 0.076 | 11.0 | 34.0 | 0.9978 | 3.51 | 0.56 | 9.4 | 5 | 0 |
| 1 | 7.8 | 0.88 | 0.00 | 2.6 | 0.098 | 25.0 | 67.0 | 0.9968 | 3.20 | 0.68 | 9.8 | 5 | 0 |
| 2 | 7.8 | 0.76 | 0.04 | 2.3 | 0.092 | 15.0 | 54.0 | 0.9970 | 3.26 | 0.65 | 9.8 | 5 | 0 |
| 3 | 11.2 | 0.28 | 0.56 | 1.9 | 0.075 | 17.0 | 60.0 | 0.9980 | 3.16 | 0.58 | 9.8 | 6 | 1 |
| 4 | 7.4 | 0.70 | 0.00 | 1.9 | 0.076 | 11.0 | 34.0 | 0.9978 | 3.51 | 0.56 | 9.4 | 5 | 0 |
df_wines.describe()
| fixed acidity | volatile acidity | citric acid | residual sugar | chlorides | free sulfur dioxide | total sulfur dioxide | density | pH | sulphates | alcohol | quality | is_good | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 | 1599.000000 |
| mean | 8.319637 | 0.527821 | 0.270976 | 2.538806 | 0.087467 | 15.874922 | 46.467792 | 0.996747 | 3.311113 | 0.658149 | 10.422983 | 5.636023 | 0.534709 |
| std | 1.741096 | 0.179060 | 0.194801 | 1.409928 | 0.047065 | 10.460157 | 32.895324 | 0.001887 | 0.154386 | 0.169507 | 1.065668 | 0.807569 | 0.498950 |
| min | 4.600000 | 0.120000 | 0.000000 | 0.900000 | 0.012000 | 1.000000 | 6.000000 | 0.990070 | 2.740000 | 0.330000 | 8.400000 | 3.000000 | 0.000000 |
| 25% | 7.100000 | 0.390000 | 0.090000 | 1.900000 | 0.070000 | 7.000000 | 22.000000 | 0.995600 | 3.210000 | 0.550000 | 9.500000 | 5.000000 | 0.000000 |
| 50% | 7.900000 | 0.520000 | 0.260000 | 2.200000 | 0.079000 | 14.000000 | 38.000000 | 0.996750 | 3.310000 | 0.620000 | 10.200000 | 6.000000 | 1.000000 |
| 75% | 9.200000 | 0.640000 | 0.420000 | 2.600000 | 0.090000 | 21.000000 | 62.000000 | 0.997835 | 3.400000 | 0.730000 | 11.100000 | 6.000000 | 1.000000 |
| max | 15.900000 | 1.580000 | 1.000000 | 15.500000 | 0.611000 | 72.000000 | 289.000000 | 1.003690 | 4.010000 | 2.000000 | 14.900000 | 8.000000 | 1.000000 |
X = df_wines.drop(columns = ['quality','is_good'])
y = df_wines[['is_good']]
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 1613)
import warnings
warnings.filterwarnings('ignore') # kwestia nieistotnego komunikatu
xgb_model = xgb.XGBClassifier(objective = "binary:logistic", seed = 1613, use_label_encoder=False)
xgb_model.fit(X_train, y_train["is_good"])
preds = xgb_model.predict(X_test)
[17:28:45] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.3.0/src/learner.cc:1061: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
from sklearn.metrics import accuracy_score
pred = pd.DataFrame({'predicted':preds})
accuracy_score(y_test,pred)
0.81
explainer = dx.Explainer(xgb_model, X_train, y_train)
Preparation of a new explainer is initiated -> data : 1199 rows 11 cols -> target variable : Parameter 'y' was a pandas.DataFrame. Converted to a numpy.ndarray. -> target variable : 1199 values -> model_class : xgboost.sklearn.XGBClassifier (default) -> label : Not specified, model's class short name will be used. (default) -> predict function : <function yhat_proba_default at 0x00000122CB93F430> will be used (default) -> predict function : Accepts pandas.DataFrame and numpy.ndarray. -> predicted values : min = 5.28e-05, mean = 0.533, max = 1.0 -> model type : classification will be used (default) -> residual function : difference between y and yhat (default) -> residuals : min = -0.202, mean = -2.46e-05, max = 0.185 -> model_info : package xgboost A new explainer has been created!
Na początku pokażemy wartości SHAP zmiennych dla całego zbioru X_train
explainer2 = shap.TreeExplainer(xgb_model)
shap_values_every = explainer2.shap_values(X_train)
shap.summary_plot(shap_values_every, X_train)
shap.summary_plot(shap_values_every, X_train, plot_type="bar")
Jak widać średnio największy wpływ na predykcje naszego modelu mają zmienne alkohol,sulphates,volatile acidity i total sulfur dioxide czego mogliśmy się spodziewać po badaniu korelacji zmiennych z naszą zmienną celu podczas eksploracji danych. Reszta zmiennych wydaje się mieć podobny wpływ na nasz model. Zobaczymy czy wszytkie badane przez nas obserwacje będą miały podobne wartości SHAP.
X_train.head(1)
| fixed acidity | volatile acidity | citric acid | residual sugar | chlorides | free sulfur dioxide | total sulfur dioxide | density | pH | sulphates | alcohol | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1437 | 6.8 | 0.915 | 0.29 | 4.8 | 0.07 | 15.0 | 39.0 | 0.99577 | 3.53 | 0.54 | 11.1 |
pred_one = explainer.predict(X_train.head(1))
print("prawdziwa wartość: "+str(y_train.iloc[0,:][0])+", predyckja: " + str(pred_one[0]))
prawdziwa wartość: 0, predyckja: 0.016037062
pp_shap = explainer.predict_parts(X_train.iloc[0,:],type='shap')
pp_shap
| variable | contribution | variable_name | variable_value | sign | label | B | |
|---|---|---|---|---|---|---|---|
| 0 | density = 0.9958 | -0.037618 | density | 0.99577 | -1.0 | XGBClassifier | 1 |
| 1 | total sulfur dioxide = 39.0 | -0.011693 | total sulfur dioxide | 39.00000 | -1.0 | XGBClassifier | 1 |
| 2 | chlorides = 0.07 | -0.031835 | chlorides | 0.07000 | -1.0 | XGBClassifier | 1 |
| 3 | fixed acidity = 6.8 | 0.017702 | fixed acidity | 6.80000 | 1.0 | XGBClassifier | 1 |
| 4 | volatile acidity = 0.915 | -0.151669 | volatile acidity | 0.91500 | -1.0 | XGBClassifier | 1 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 6 | residual sugar = 4.8 | -0.020506 | residual sugar | 4.80000 | -1.0 | XGBClassifier | 0 |
| 7 | free sulfur dioxide = 15.0 | 0.016476 | free sulfur dioxide | 15.00000 | 1.0 | XGBClassifier | 0 |
| 8 | fixed acidity = 6.8 | -0.010483 | fixed acidity | 6.80000 | -1.0 | XGBClassifier | 0 |
| 9 | total sulfur dioxide = 39.0 | 0.007086 | total sulfur dioxide | 39.00000 | 1.0 | XGBClassifier | 0 |
| 10 | citric acid = 0.29 | 0.003802 | citric acid | 0.29000 | 1.0 | XGBClassifier | 0 |
286 rows × 7 columns
pp_shap.plot()
Już w pierwszej badanej przez nas obserwacji wsród trzech zmiennych najbardziej wpływających na predykcje modelu nie ma alkoholu, a kwasowość lotna wpływa negatywnie na predykcje naszego modelu. Widać więc, że wartości SHAP zmiennych w tej obserwacji nieco różnią się od uśrednionych dla całego zbioru X_train.
explainer.predict_parts(X_train.iloc[97,:],type='shap').plot()
explainer.predict_parts(X_train.iloc[258,:],type='shap').plot()
Pierwsze trzy najważniejsze zmienne dla obydwu oberwacji są różne. Dla pierwszej obserwacji zmienną, która najbardziej wpływa na predykcję modelu jest alkohol. Jego wskaźnik contribution wynosi 0.384 i prawie samodzielnie decyduje o tym czy wino jest uznane przez model za dobre. Może być to spowodowane tym, że wartości reszty zmiennych są zbliżone do ich median. Co ciekawe dla drugiej obserwacji duże znaczenie ma PH, które średnio jest namniej znaczącą zmienną. Dodatkowo w drugiej obserwacji wsród najwazniejszych zmiennych nie ma alkoholu.
explainer.predict_parts(X_train.iloc[218,:],type='shap').plot()
explainer.predict_parts(X_train.iloc[912,:],type='shap').plot()
Dla powyższych obserwacji zmiennymi posiadającymi przeciwne wpływy na wynik predykcji to alcohol,sulphates,total sulfur dioxide,fixed acidity,residual sugar i PH. Czasami małe różnice w wartościach na przykład zawartość siarczynów (sulphates) albo alkoholu mogą mieć kompletnie inny wpływ na predykcję danej obserwacji. Także wartość nieodfermentowanego cukru (residual sugar) jest prawie taka sama w obu obserwacjach (zakres wartości to 14.6). Mimo to jednak w obu obserwacjach wartość bezwględna wkładu(contribution) obu tych zmiennych różni się ok. 13 krotnie i w pierwszej obserwacji ma pozytywny efekt, a w drugiej negatywny. Dzieje się tak poprzez wpływ innych zmiennych na predykcję modelu.